﻿using yzrilyzr.util;
using Math = java.lang.Math;

namespace FAQ_UI
{
	public class Scroller
	{
		private static readonly float DEFAULT_DURATION = 250f;
		private const int SCROLL_MODE = 0;
		private const int FLING_MODE = 1;
		private static readonly float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
		private static readonly float START_TENSION = 0.5f;
		private static readonly float END_TENSION = 1.0f;
		private static readonly float P1 = START_TENSION * INFLEXION;
		private static readonly float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
		private static readonly int NB_SAMPLES = 100;
		private static readonly float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
		private static readonly float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
		private static readonly float DECELERATION_RATE = (float)(Math.log(0.78) / Math.log(0.9));

		static Scroller()
		{
			float x_min = 0.0f;
			float y_min = 0.0f;
			for (int i = 0; i < NB_SAMPLES; i++)
			{
				float alpha = (float)i / NB_SAMPLES;
				float x_max = 1.0f;
				float x, tx, coef;
				while (true)
				{
					x = x_min + (x_max - x_min) / 2.0f;
					coef = 3.0f * x * (1.0f - x);
					tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
					if (Math.abs(tx - alpha) < 1E-5) break;
					if (tx > alpha) x_max = x;
					else x_min = x;
				}
				SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
				float y_max = 1.0f;
				float y, dy;
				while (true)
				{
					y = y_min + (y_max - y_min) / 2.0f;
					coef = 3.0f * y * (1.0f - y);
					dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
					if (Math.abs(dy - alpha) < 1E-5) break;
					if (dy > alpha) y_max = y;
					else y_min = y;
				}
				SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
			}
			SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
		}

		private Interpolator mInterpolator;
		private float mPpi;
		private bool mFlywheel;

		// A context-specific coefficient adjusted to physical values.
		private float mPhysicalCoeff;

		private float mMode;
		private float mStartX;
		private float mStartY;
		private float mFinalX;
		private float mFinalY;
		private float mMinX;
		private float mMaxX;
		private float mMinY;
		private float mMaxY;
		private float mCurrX;
		private float mCurrY;
		private long mStartTime;
		private float mDuration;
		private float mDurationReciprocal;
		private float mDeltaX;
		private float mDeltaY;
		private bool mFinished;
		private float mVelocity;
		private float mCurrVelocity;
		private float mDistance;
		private float mFlingFriction = 0.015f;
		private float mDeceleration;
		/**
         * Create a Scroller with the default duration and floaterpolator.
         */

		public Scroller() : this(null)
		{
		}

		/**
         * Create a Scroller with the specified floaterpolator. If the floaterpolator is
         * null, the default (viscous) floaterpolator will be used. "Flywheel" behavior will
         * be in effect for apps targeting Honeycomb or newer.
         */

		public Scroller(Interpolator floaterpolator) : this(floaterpolator, true)
		{
			;
		}

		public Scroller(Interpolator floaterpolator, bool flywheel)
		{
			mFinished = true;
			if (floaterpolator == null)
			{
				mInterpolator = new ViscousFluidInterpolator();
			}
			else
			{
				mInterpolator = floaterpolator;
			}
			mPpi = 300;
			mDeceleration = computeDeceleration(mFlingFriction);
			mFlywheel = flywheel;
			mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
		}

		private float computeDeceleration(float friction)
		{
			return 9.8f   // g (m/s^2)
				   * 39.37f               // inch/meter
				   * mPpi                 // pixels per inch
				   * friction;
		}

		public void setFriction(float friction)
		{
			mDeceleration = computeDeceleration(friction);
			mFlingFriction = friction;
		}

		/**
         * Returns whether the scroller has finished scrolling.
         *
         * @return True if the scroller has finished scrolling, false otherwise.
         */

		public bool isFinished()
		{
			return mFinished;
		}

		/**
         * Force the finished field to a particular value.
         *
         * @param finished The new finished value.
         */

		public void forceFinished(bool finished)
		{
			mFinished = finished;
		}

		/**
         * Returns how long the scroll event will take, in milliseconds.
         *
         * @return The duration of the scroll in milliseconds.
         */

		public float getDuration()
		{
			return mDuration;
		}

		/**
         * Returns the current X offset in the scroll.
         *
         * @return The new X offset as an absolute distance from the origin.
         */

		public float getCurrX()
		{
			return mCurrX;
		}

		/**
         * Returns the current Y offset in the scroll.
         *
         * @return The new Y offset as an absolute distance from the origin.
         */

		public float getCurrY()
		{
			return mCurrY;
		}

		/**
         * Returns the start X offset in the scroll.
         *
         * @return The start X offset as an absolute distance from the origin.
         */

		public float getStartX()
		{
			return mStartX;
		}

		/**
         * Returns the start Y offset in the scroll.
         *
         * @return The start Y offset as an absolute distance from the origin.
         */

		public float getStartY()
		{
			return mStartY;
		}

		/**
         * Returns where the scroll will end. Valid only for "fling" scrolls.
         *
         * @return The  X offset as an absolute distance from the origin.
         */

		public float getFinalX()
		{
			return mFinalX;
		}

		/**
         * Sets the  position (X) for this scroller.
         *
         * @param newX The new X offset as an absolute distance from the origin.
         * @see #extendDuration(float)
         * @see #setFinalY(float)
         */

		public void setFinalX(float newX)
		{
			mFinalX = newX;
			mDeltaX = mFinalX - mStartX;
			mFinished = false;
		}

		/**
         * Returns where the scroll will end. Valid only for "fling" scrolls.
         *
         * @return The  Y offset as an absolute distance from the origin.
         */

		public float getFinalY()
		{
			return mFinalY;
		}

		/**
         * Sets the  position (Y) for this scroller.
         *
         * @param newY The new Y offset as an absolute distance from the origin.
         * @see #extendDuration(float)
         * @see #setFinalX(float)
         */

		public void setFinalY(float newY)
		{
			mFinalY = newY;
			mDeltaY = mFinalY - mStartY;
			mFinished = false;
		}

		/**
         * Call this when you want to know the new location.  If it returns true,
         * the animation is not yet finished.
         */

		private long getMs()
		{
			return java.lang.System.currentTimeMillis();
		}

		public bool computeScrollOffset()
		{
			if (mFinished)
			{
				return false;
			}
			float timePassed = (getMs() - mStartTime);
			Logger.getDef().i("timePassed", timePassed);
			if (timePassed < mDuration)
			{
				switch (mMode)
				{
					case SCROLL_MODE:
						float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
						mCurrX = mStartX + Math.round(x * mDeltaX);
						mCurrY = mStartY + Math.round(x * mDeltaY);
						break;

					case FLING_MODE:
						float t = (float)timePassed / mDuration;
						int index = (int)(NB_SAMPLES * t);
						float distanceCoef = 1.0f;
						float velocityCoef = 0.0f;
						if (index < NB_SAMPLES)
						{
							float t_inf = (float)index / NB_SAMPLES;
							float t_sup = (float)(index + 1) / NB_SAMPLES;
							float d_inf = SPLINE_POSITION[index];
							float d_sup = SPLINE_POSITION[index + 1];
							velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
							distanceCoef = d_inf + (t - t_inf) * velocityCoef;
						}
						mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
						mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
						// Pin to mMinX <= mCurrX <= mMaxX
						mCurrX = Math.min(mCurrX, mMaxX);
						mCurrX = Math.max(mCurrX, mMinX);
						mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
						// Pin to mMinY <= mCurrY <= mMaxY
						mCurrY = Math.min(mCurrY, mMaxY);
						mCurrY = Math.max(mCurrY, mMinY);
						if (mCurrX == mFinalX && mCurrY == mFinalY)
						{
							mFinished = true;
						}
						break;
				}
			}
			else
			{
				mCurrX = mFinalX;
				mCurrY = mFinalY;
				mFinished = true;
			}
			return true;
		}

		/**
         * Start scrolling by providing a starting pofloat and the distance to travel.
         * The scroll will use the default value of 250 milliseconds for the
         * duration.
         *
         * @param startX Starting horizontal scroll offset in pixels. Positive
         *               numbers will scroll the content to the left.
         * @param startY Starting vertical scroll offset in pixels. Positive numbers
         *               will scroll the content up.
         * @param dx     Horizontal distance to travel. Positive numbers will scroll the
         *               content to the left.
         * @param dy     Vertical distance to travel. Positive numbers will scroll the
         *               content up.
         */

		public void startScroll(float startX, float startY, float dx, float dy)
		{
			startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
		}

		/**
         * Start scrolling by providing a starting pofloat, the distance to travel,
         * and the duration of the scroll.
         *
         * @param startX   Starting horizontal scroll offset in pixels. Positive
         *                 numbers will scroll the content to the left.
         * @param startY   Starting vertical scroll offset in pixels. Positive numbers
         *                 will scroll the content up.
         * @param dx       Horizontal distance to travel. Positive numbers will scroll the
         *                 content to the left.
         * @param dy       Vertical distance to travel. Positive numbers will scroll the
         *                 content up.
         * @param duration Duration of the scroll in milliseconds.
         */

		public void startScroll(float startX, float startY, float dx, float dy, float duration)
		{
			mMode = SCROLL_MODE;
			mFinished = false;
			mDuration = duration;
			mStartTime = getMs();
			mStartX = startX;
			mStartY = startY;
			mFinalX = startX + dx;
			mFinalY = startY + dy;
			mDeltaX = dx;
			mDeltaY = dy;
			mDurationReciprocal = 1.0f / (float)mDuration;
		}

		/**
         * Start scrolling based on a fling gesture. The distance travelled will
         * depend on the initial velocity of the fling.
         *
         * @param startX    Starting pofloat of the scroll (X)
         * @param startY    Starting pofloat of the scroll (Y)
         * @param velocityX Initial velocity of the fling (X) measured in pixels per
         *                  second.
         * @param velocityY Initial velocity of the fling (Y) measured in pixels per
         *                  second
         * @param minX      Minimum X value. The scroller will not scroll past this
         *                  pofloat.
         * @param maxX      Maximum X value. The scroller will not scroll past this
         *                  pofloat.
         * @param minY      Minimum Y value. The scroller will not scroll past this
         *                  pofloat.
         * @param maxY      Maximum Y value. The scroller will not scroll past this
         *                  pofloat.
         */

		public void fling(float startX, float startY, float velocityX, float velocityY, float minX, float maxX, float minY, float maxY)
		{
			// Continue a scroll or fling in progress
			if (mFlywheel && !mFinished)
			{
				float oldVel = getCurrVelocity();
				float dx = (float)(mFinalX - mStartX);
				float dy = (float)(mFinalY - mStartY);
				float hyp = (float)Math.hypot(dx, dy);
				float ndx = dx / hyp;
				float ndy = dy / hyp;
				float oldVelocityX = ndx * oldVel;
				float oldVelocityY = ndy * oldVel;
				if (Math.signum(velocityX) == Math.signum(oldVelocityX) && Math.signum(velocityY) == Math.signum(oldVelocityY))
				{
					velocityX += (float)oldVelocityX;
					velocityY += (float)oldVelocityY;
				}
			}
			mMode = FLING_MODE;
			mFinished = false;
			float velocity = (float)Math.hypot(velocityX, velocityY);
			mVelocity = velocity;
			mDuration = getSplineFlingDuration(velocity);
			mStartTime = getMs();
			mStartX = startX;
			mStartY = startY;
			float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
			float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
			double totalDistance = getSplineFlingDistance(velocity);
			mDistance = (float)(totalDistance * Math.signum(velocity));
			mMinX = minX;
			mMaxX = maxX;
			mMinY = minY;
			mMaxY = maxY;
			mFinalX = startX + (float)Math.round(totalDistance * coeffX);
			// Pin to mMinX <= mFinalX <= mMaxX
			mFinalX = Math.min(mFinalX, mMaxX);
			mFinalX = Math.max(mFinalX, mMinX);
			mFinalY = startY + (float)Math.round(totalDistance * coeffY);
			// Pin to mMinY <= mFinalY <= mMaxY
			mFinalY = Math.min(mFinalY, mMaxY);
			mFinalY = Math.max(mFinalY, mMinY);
		}

		/**
         * Returns the current velocity.
         *
         * @return The original velocity less the deceleration. Result may be
         * negative.
         */

		public float getCurrVelocity()
		{
			return mMode == FLING_MODE ? mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
		}

		private float getSplineFlingDuration(float velocity)
		{
			double l = getSplineDeceleration(velocity);
			double decelMinusOne = DECELERATION_RATE - 1.0;
			return (float)(1000.0 * Math.exp(l / decelMinusOne));
		}

		private double getSplineFlingDistance(float velocity)
		{
			double l = getSplineDeceleration(velocity);
			double decelMinusOne = DECELERATION_RATE - 1.0;
			return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
		}

		/**
         * Returns the time elapsed since the beginning of the scrolling.
         *
         * @return The elapsed time in milliseconds.
         */

		public float timePassed()
		{
			return (float)(getMs() - mStartTime);
		}

		private double getSplineDeceleration(float velocity)
		{
			return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
		}

		/**
         * Stops the animation. Contrary to ,
         * aborting the animating cause the scroller to move to the  x and y
         * position
         *
         * @see #forceFinished(bool)
         */

		public void abortAnimation()
		{
			mCurrX = mFinalX;
			mCurrY = mFinalY;
			mFinished = true;
		}

		/**
         * Extend the scroll animation. This allows a running animation to scroll
         * further and longer, when used with .
         *
         * @param extend Additional time to scroll in milliseconds.
         * @see #setFinalX(float)
         * @see #setFinalY(float)
         */

		public void extendDuration(float extend)
		{
			float passed = timePassed();
			mDuration = passed + extend;
			mDurationReciprocal = 1.0f / mDuration;
			mFinished = false;
		}

		/**
         * @hide
         */

		public bool isScrollingInDirection(float xvel, float yvel)
		{
			return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && Math.signum(yvel) == Math.signum(mFinalY - mStartY);
		}

		private class ViscousFluidInterpolator : Interpolator
		{
			/**
             * Controls the viscous fluid effect (how much of it).
             */
			private static float VISCOUS_FLUID_SCALE = 8.0f;
			private static float VISCOUS_FLUID_NORMALIZE;
			private static float VISCOUS_FLUID_OFFSET;

			static ViscousFluidInterpolator()
			{
				// must be set to 1.0 (used in viscousFluid())
				VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
				// account for very small floating-pofloat error
				VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
			}

			public float getInterpolation(float input)
			{
				float floaterpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
				if (floaterpolated > 0)
				{
					return floaterpolated + VISCOUS_FLUID_OFFSET;
				}
				return floaterpolated;
			}

			private static float viscousFluid(float x)
			{
				x *= VISCOUS_FLUID_SCALE;
				if (x < 1.0f)
				{
					x -= (1.0f - (float)Math.exp(-x));
				}
				else
				{
					float start = 0.36787944117f;   // 1/e == exp(-1)
					x = 1.0f - (float)Math.exp(1.0f - x);
					x = start + x * (1.0f - start);
				}
				return x;
			}
		}
	}
}